﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Net.Nntp.Cache
{
    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Security.Cryptography;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>Reports progress to the console.</summary>
    internal sealed class ConsoleObserver : ISendReceiveProgressObserver
    {
        private readonly object padLock = new object();
        private readonly Dictionary<object, int> callCounts = new Dictionary<object, int>();
        private readonly bool noNetworkConnection;
        private int start;
        private Exception unexpectedException;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnConnecting"/>.</summary>
        public IDisposable OnConnecting(string hostName)
        {
            this.AddCall<string, IDisposable>(this.OnConnecting);
            this.start = Environment.TickCount;
            Console.WriteLine("Connecting to {0} ...", hostName);
            return this.noNetworkConnection ? null : new AlwaysOnNetworkConnection();
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnConnected"/>.</summary>
        public void OnConnected(string hostName)
        {
            this.AddCall<string>(this.OnConnected);
            Console.WriteLine("Connected to {0} (@{1}ms).", hostName, this.ElapsedTime);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnSendProgressChanged"/>.</summary>
        public void OnSendProgressChanged(int percent)
        {
            this.AddCall<int>(this.OnSendProgressChanged);
            Console.WriteLine("Sending articles: {0}% (@{1}ms) ...", percent, this.ElapsedTime);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnHeadersProgressChanged"/>.</summary>
        public void OnHeadersProgressChanged(string groupDisplayName, int percent)
        {
            this.AddCall<string, int>(this.OnHeadersProgressChanged);
            Console.WriteLine(
                "Receiving headers from {0}: {1}% (@{2}ms) ...", groupDisplayName, percent, this.ElapsedTime);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnBodiesProgressChanged"/>.</summary>
        public void OnBodiesProgressChanged(string groupDisplayName, int percent)
        {
            this.AddCall<string, int>(this.OnBodiesProgressChanged);
            Console.WriteLine(
                "Receiving bodies from {0}: {1}% (@{2}ms) ...", groupDisplayName, percent, this.ElapsedTime);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnFinished"/>.</summary>
        public void OnFinished()
        {
            this.AddCall(this.OnFinished);
            Console.WriteLine("Send/Receive finished. (@{0}ms)", this.ElapsedTime);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnStopped"/>.</summary>
        public void OnStopped()
        {
            this.AddCall(this.OnStopped);
            Console.WriteLine("Send/Receive stopped. (@{0}ms)", this.ElapsedTime);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnError"/>.</summary>
        public bool OnError(string hostName, string groupName, Exception exception)
        {
            bool handled =
                TryHandle<PostingNotPermittedException>(exception) ||
                TryHandle<PostingFailedException>(exception) ||
                TryHandle<UnknownCommandException>(exception) ||
                TryHandle<ServiceUnavailableException>(exception) ||
                TryHandle<ServerFaultException>(exception) ||
                TryHandle<NoSuchGroupException>(exception) ||
                TryHandle<MissingAuthenticationException>(exception) ||
                TryHandle<AuthenticationFailedException>(exception) ||
                TryHandle<CryptographicException>(exception) ||
                TryHandle<MissingPrivacyException>(exception) ||
                TryHandle<UnexpectedResponseException>(exception) ||
                TryHandle<HostUnreachableException>(exception) ||
                TryHandle<ConnectionLostException>(exception) ||
                TryHandle<WriteFailedException>(exception);

            if (!handled)
            {
                this.AddCall(exception);
                this.SetException(exception);
            }

            return true;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal ConsoleObserver() : this(false)
        {
        }

        internal ConsoleObserver(bool noNetworkConnection)
        {
            this.noNetworkConnection = noNetworkConnection;
        }

        internal void AssertCallCount<T>(Action<T> call, int expectedCount)
        {
            this.AssertCallCount((object)call, expectedCount);
        }

        internal void AssertCallCount<T, U>(Action<T, U> call, int expectedCount)
        {
            this.AssertCallCount((object)call, expectedCount);
        }

        internal void AssertCallCount<T, U>(Func<T, U> call, int expectedCount)
        {
            this.AssertCallCount((object)call, expectedCount);
        }

        internal void AssertCallCount(Exception exception, int expectedCount)
        {
            this.AssertCallCount(exception.GetType(), expectedCount);
        }

        internal void AssertNoExceptions()
        {
            lock (this.padLock)
            {
                foreach (var pair in this.callCounts)
                {
                    var type = pair.Key as Type;

                    if (type != null)
                    {
                        Assert.AreEqual(0, pair.Value, "Exception: " + type);
                    }
                }
            }
        }

        internal void AssertTotalCallCount(int expectedCount)
        {
            int count = 0;

            lock (this.padLock)
            {
                foreach (var pair in this.callCounts)
                {
                    count += pair.Value;
                }
            }

            Assert.AreEqual(expectedCount, count);
        }

        internal void RethrowException()
        {
            lock (this.padLock)
            {
                if (this.unexpectedException != null)
                {
                    throw new TargetInvocationException(this.unexpectedException);
                }
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private int ElapsedTime
        {
            get { return Environment.TickCount - this.start; }
        }

        private bool TryHandle<TException>(Exception exception) where TException : Exception
        {
            bool result = exception is TException;

            if (result)
            {
                this.AddCall(exception);
            }

            return result;
        }

        private void SetException(Exception ex)
        {
            lock (this.padLock)
            {
                this.unexpectedException = ex;
            }
        }

        private void AddCall(Action call)
        {
            this.AddCall((object)call);
        }

        private void AddCall<T>(Action<T> call)
        {
            this.AddCall((object)call);
        }

        private void AddCall<T, U>(Action<T, U> call)
        {
            this.AddCall((object)call);
        }

        private void AddCall<T, U>(Func<T, U> call)
        {
            this.AddCall((object)call);
        }

        private void AddCall(Exception ex)
        {
            this.AddCall(ex.GetType());
        }

        private void AssertCallCount(object call, int expectedCount)
        {
            int count;

            lock (this.padLock)
            {
                this.callCounts.TryGetValue(call, out count);
            }

            Assert.AreEqual(expectedCount, count);
        }

        private void AddCall(object call)
        {
            lock (this.padLock)
            {
                int count;
                this.callCounts.TryGetValue(call, out count);
                this.callCounts[call] = count + 1;
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Represents an always-on network connection.</summary>
        private sealed class AlwaysOnNetworkConnection : IDisposable
        {
            /// <summary>See <see cref="IDisposable.Dispose"/>.</summary>
            public void Dispose()
            {
            }
        }
    }
}
